package com.hzw.saas.service.storage.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hzw.saas.api.storage.ICatalogService;
import com.hzw.saas.api.storage.IResourceFileService;
import com.hzw.saas.api.storage.IResourceFolderService;
import com.hzw.saas.api.storage.InxChunkService;
import com.hzw.saas.api.storage.bo.ResourceFileBO;
import com.hzw.saas.api.storage.bo.ResourceFileConflictBO;
import com.hzw.saas.api.storage.bo.ResourceFileStorageBO;
import com.hzw.saas.api.storage.bo.ResourceFolderStructBO;
import com.hzw.saas.api.storage.enums.FileIntegrityEnum;
import com.hzw.saas.api.storage.enums.ResourceStatusEnum;
import com.hzw.saas.api.storage.exception.FileConflictException;
import com.hzw.saas.api.storage.query.ResourceFileUpdateQuery;
import com.hzw.saas.common.config.exception.DefaultSaasException;
import com.hzw.saas.common.config.query.LogicQuery;
import com.hzw.saas.common.config.util.MybatisPlusUtil;
import com.hzw.saas.common.storage.enums.StorageModeEnum;
import com.hzw.saas.common.storage.pojo.FileStorage;
import com.hzw.saas.common.storage.service.IFileStorageService;
import com.hzw.saas.common.storage.service.IS3FileService;
import com.hzw.saas.common.util.Json;
import com.hzw.saas.common.util.MapperUtil;
import com.hzw.saas.common.util.ObjFieldUtil;
import com.hzw.saas.common.util.SnowFlakeUtil;
import com.hzw.saas.common.util.constants.PathConsts;
import com.hzw.saas.common.util.enums.AssertEnum;
import com.hzw.saas.common.util.enums.ConflictModeEnum;
import com.hzw.saas.common.util.exception.ForbiddenException;
import com.hzw.saas.common.util.exception.LostException;
import com.hzw.saas.service.storage.config.StorageConfig;
import com.hzw.saas.service.storage.mapper.ResourceFileMapper;
import com.hzw.saas.service.storage.model.ResourceFile;
import com.hzw.saas.service.storage.pojo.FileStorageInfo;
import com.hzw.saas.service.storage.util.StorageUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 资源表 服务实现类
 * </p>
 *
 * @author zzl
 * @since 2020-12-24
 */
@Service(ResourceFileServiceImpl.BEAN_NAME)
@RequiredArgsConstructor
@Slf4j
public class ResourceFileServiceImpl extends ServiceImpl<ResourceFileMapper, ResourceFile> implements IResourceFileService, IService<ResourceFile> {

    public static final String BEAN_NAME = "resourceFileServiceImpl";

    private final ApplicationEventPublisher applicationEventPublisher;

    // 存储服务
    private final StorageConfig storageConfig;
    private final ICatalogService catalogService;
    private final IFileStorageService fileStorageService;
    private final IS3FileService s3FileService;

    @Resource
    private InxChunkService inxChunkService;

    @Resource
    private StorageModeHandler storageModeService;

    @Resource
    private IResourceFolderService folderService;

    @PostConstruct
    public void init() {
    }

    @Override
    public ResourceFileBO getFile(String pid, AssertEnum assertEnum) {
        return this.getFile(pid,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()),
            assertEnum);
    }

    @Override
    public ResourceFileBO getFile(String pid, LogicQuery<Integer> status, LogicQuery<Integer> integrity, AssertEnum assertEnum) {
        ResourceFile fileDO = this.getFileDO(this.select(), pid, status, integrity, assertEnum);
        return MapperUtil.nf().map(fileDO, ResourceFileBO.class);
    }

    @Override
    public List<ResourceFileBO> listFiles(List<String> pidList) {
        return this.listFiles(pidList,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()));
    }

    @Override
    public List<ResourceFileBO> listFiles(List<String> pidList, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        List<ResourceFile> fileDOList = this.listFileDOs(this.select(), pidList, status, integrity);
        return MapperUtil.nf().mapAsList(fileDOList, ResourceFileBO.class);
    }

    @Override
    public List<ResourceFileBO> listSubFiles(String supId) {
        return this.listSubFiles(supId,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()));
    }

    @Override
    public List<ResourceFileBO> listSubFiles(String supId, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        List<ResourceFile> list = this.listSubFileDOs(this.select(), supId, status, integrity);
        return MapperUtil.nf().mapAsList(list, ResourceFileBO.class);
    }

    @Override
    public List<ResourceFileBO> listSubFiles(List<String> supIdList) {
        return this.listSubFiles(supIdList,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()));
    }


    @Override
    public List<ResourceFileBO> listSubFiles(List<String> supIdList, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        List<ResourceFile> list = this.listSubFileDOs(this.select(), supIdList, status, integrity);
        return MapperUtil.nf().mapAsList(list, ResourceFileBO.class);
    }

    @Override
    public List<ResourceFileBO> listUserFiles(String userId, String spaceId) {
        return this.listUserFiles(userId, spaceId,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()));
    }


    @Override
    public List<ResourceFileBO> listUserFiles(String userId, String spaceId, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        List<ResourceFile> list = this.listUserFileDOs(this.select(), userId, spaceId, status, integrity);
        return MapperUtil.nf().mapAsList(list, ResourceFileBO.class);
    }


    @Override
    public List<String> listSubFilePids(String supId, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        List<ResourceFile> list = this.listSubFileDOs(this.select(ResourceFile::getPid), supId, status, integrity);
        return list.stream().map(ResourceFile::getPid).collect(Collectors.toList());
    }

    @Override
    public List<String> listSubFilePids(List<String> supIds, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        List<ResourceFile> list = this.listSubFileDOs(this.select(ResourceFile::getPid), supIds, status, integrity);
        return list.stream().map(ResourceFile::getPid).collect(Collectors.toList());
    }

    @Override
    public ResourceFileStorageBO getFileWithStorage(String pid, LogicQuery<Integer> status, LogicQuery<Integer> integrity, AssertEnum assertEnum) {
        ResourceFile fileDO = this.getFileDO(this.select(), pid, status, integrity, assertEnum);
        return MapperUtil.nf().map(fileDO, ResourceFileStorageBO.class);
    }

    @Override
    public ResourceFileBO getLatestFile(String vid, LogicQuery<Integer> status, LogicQuery<Integer> integrity, AssertEnum assertEnum) {
        if (StrUtil.isBlank(vid)) {
            return null;
        }
        LambdaQueryChainWrapper<ResourceFile> query = super.lambdaQuery()
            .eq(ResourceFile::getVid, vid)
            .orderByDesc(ResourceFile::getCreateTime);
        StorageUtil.addCondition(query, status, integrity);
        MybatisPlusUtil.limitOne(query);
        ResourceFile one = query.one();
        AssertEnum.assertResult(assertEnum, one, "文件");
        return MapperUtil.nf().map(one, ResourceFileBO.class);
    }

    @Override
    public boolean isNameExist(String supId, String fileName, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        if (StrUtil.isBlank(supId)) {
            return false;
        }
        LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryBySupId(
            this.select(ResourceFile::getPid), supId, fileName, status, integrity);
        Integer count = query.count();
        return !Objects.equals(count, 0);
    }

    @Override
    public int countNumberByUser(String userId, List<String> spaceIds, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryBySpaces(
            this.select(ResourceFile::getPid), userId, spaceIds, status, integrity);
        return query.count();
    }

    @Override
    public int countNumberBySup(String supId, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryBySupId(
            this.select(ResourceFile::getPid), supId, null, status, integrity);
        return query.count();
    }

    @Override
    public long countSizeByPid(List<String> pidList, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        if (CollectionUtil.isEmpty(pidList)) {
            return 0L;
        }
        LambdaQueryWrapper<ResourceFile> query = this.buildSumLengthQuery(status, integrity);
        query.in(ResourceFile::getPid, pidList);
        return this.getSumLength(query);
    }

    @Override
    public long countSizeByUser(String userId, List<String> spaceIds, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        LambdaQueryWrapper<ResourceFile> query = this.buildSumLengthQuery(status, integrity);
        query.eq(Objects.nonNull(userId), ResourceFile::getCreatorId, userId)
            .in(CollUtil.isNotEmpty(spaceIds), ResourceFile::getSpaceId, spaceIds);
        return this.getSumLength(query);
    }

    @Override
    public long countSizeBySupId(String supId, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        if (StrUtil.isBlank(supId)) {
            return 0L;
        }
        LambdaQueryWrapper<ResourceFile> query = this.buildSumLengthQuery(status, integrity);
        query.eq(ResourceFile::getSupId, supId);
        return this.getSumLength(query);
    }

    @Override
    public long countSizeBySupId(List<String> supIdList, LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        if (CollUtil.isEmpty(supIdList)) {
            return 0L;
        }
        LambdaQueryWrapper<ResourceFile> query = this.buildSumLengthQuery(status, integrity);
        query.in(ResourceFile::getSupId, supIdList);
        return this.getSumLength(query);
    }


    @Override
    public boolean checkAuthOwn(String userId, String fileId) {
        Integer count = super.lambdaQuery()
            .select(ResourceFile::getPid)
            .eq(ResourceFile::getPid, fileId)
            .eq(ResourceFile::getCreatorId, userId)
            .count();
        return !Objects.equals(count, 0);
    }

    @Override
    public void assertAuthOwn(String userId, String fileId) {
        boolean authOwn = this.checkAuthOwn(userId, fileId);
        Assert.isTrue(authOwn, () -> new ForbiddenException("文件无权限"));
    }

    @Override
    public boolean checkAuthOwn(String userId, List<String> fileIdList) {
        if (CollUtil.isEmpty(fileIdList)) {
            return true;
        }
        List<ResourceFile> list = super.lambdaQuery()
            .select(ResourceFile::getPid, ResourceFile::getCreatorId)
            .in(ResourceFile::getPid, fileIdList)
            .list();
        long count = list.stream().filter(file -> !Objects.equals(file.getCreatorId(), userId)).count();
        return count == 0;
    }

    @Override
    public void assertAuthOwn(String userId, List<String> fileIdList) {
        boolean auth = this.checkAuthOwn(userId, fileIdList);
        Assert.isTrue(auth, () -> new ForbiddenException("存在无权限文件"));
    }

    /**
     * 文件信息保存需进行以下校验
     * 1. 必要参数：文件名，所在目录，所有者
     * 2. 目录存在，且状态正常
     * 3. 同目录下无正常状态的同名文件
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ResourceFileStorageBO saveFile(ResourceFileStorageBO fileInfo, ConflictModeEnum conflictHandle) {
        ResourceFile fileDO = MapperUtil.nf().map(fileInfo, ResourceFile.class);
        fileDO = this.saveFileDO(fileDO, conflictHandle);
        return MapperUtil.nf().map(fileDO, ResourceFileStorageBO.class);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ResourceFileStorageBO saveFile(ResourceFileStorageBO fileBo, InputStream inputStream, ConflictModeEnum conflictModeEnum) {
        ResourceFileStorageBO fileBO = this.saveFile(fileBo, conflictModeEnum);
        String fileSupPath = catalogService.getFileSupPath(fileBO.getCatalog(), fileBO.getResourceKey());
        FileStorage fileStorage = StorageUtil.convert(fileBO, fileSupPath);
        fileStorageService.write(fileStorage, inputStream, conflictModeEnum);
        this.dealStorageMode(fileBO.getPid(), StorageModeEnum.SAVE_IN_CLOUD.code(), true);
        return fileBO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ResourceFileStorageBO saveFile(ResourceFileStorageBO fileBo, MultipartFile file, ConflictModeEnum conflictModeEnum) {
        try {
            InputStream inputStream = file.getInputStream();
            return this.saveFile(fileBo, inputStream, conflictModeEnum);
        } catch (IOException e) {
            throw DefaultSaasException.build("保存文件失败", e);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void copyFile(String srcPid, String tarSupId, String newName, ConflictModeEnum conflictHandle) {
        // 获取待复制文件
        ResourceFile file2Copy = this.getFileDO(this.select(), srcPid, AssertEnum.FOUND);
        this.getCopyFileReady(file2Copy, tarSupId, newName);
        // 复制保存
        this.saveFileDO(file2Copy, conflictHandle);
        log.debug("复制文件({srcPid: {}, tarSupId: {}, newName: {}})完成.", srcPid, tarSupId, newName);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void copyFiles(List<String> srcPidList, String tarSupId, ConflictModeEnum conflictHandle) {
        if (CollUtil.isEmpty(srcPidList)) return;
        List<ResourceFile> files2Copy = this.listFileDOs(
            this.select(), srcPidList,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()));
        this.getCopyFilesReady(files2Copy, tarSupId);
        this.saveFileDOs(files2Copy, tarSupId, conflictHandle);
        log.debug("批量复制文件({tarSupId: {}})完成.", tarSupId);
    }

    @Override
    public void copySubFiles(String srcSupId, String tarSupId, ConflictModeEnum conflictHandle) {
        List<ResourceFile> subFileDOs = this.listSubFileDOs(
            this.select(), srcSupId,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()));
        this.getCopyFilesReady(subFileDOs, tarSupId);
        this.saveFileDOs(subFileDOs, tarSupId, conflictHandle);
        log.debug("批量复制子文件({srcSupId: {}, tarSupId: {}})完成.", srcSupId, tarSupId);
    }

    /**
     * 移动文件，更新supId和spaceId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void moveFile(String pid, String tarSupId, String newName, ConflictModeEnum conflictHandle) {
        // 获取待处理文件
        ResourceFile file2Move = this.getFileDO(this.select(), pid, AssertEnum.FOUND);
        boolean needMove = this.getMoveFileReady(file2Move, tarSupId, newName);
        if (!needMove) return;
        this.updateFileDoOnMoving(file2Move, conflictHandle);
        log.debug("移动文件({srcPid: {}, tarSupId: {}, newName: {}})完成.", pid, tarSupId, newName);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void moveFiles(List<String> pidList, String tarSupId, ConflictModeEnum conflictHandle) {
        if (CollUtil.isEmpty(pidList)) return;
        List<ResourceFile> files2Move = this.listFileDOs(
            this.select(), pidList,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()));
        this.getMoveFilesReady(files2Move, tarSupId);
        this.updateFileDoOnMoving(files2Move, tarSupId, conflictHandle);
        log.debug("批量移动文件({tarSupId: {}})完成.", tarSupId);
    }

    @Override
    public void moveSubFiles(String srcSupId, String tarSupId, ConflictModeEnum conflictHandle) {
        List<ResourceFile> subFileDOs = this.listSubFileDOs(
            this.select(), srcSupId,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()));
        this.getMoveFilesReady(subFileDOs, tarSupId);
        this.updateFileDoOnMoving(subFileDOs, tarSupId, conflictHandle);
        log.debug("批量移动子文件({srcSupId: {}, tarSupId: {}})完成.", srcSupId, tarSupId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void renameFile(String pid, String newName, ConflictModeEnum conflictHandle) {
        Assert.isTrue(StrUtil.isNotBlank(newName), "文件名不可为空");
        this.moveFile(pid, null, newName, conflictHandle);
    }

    @Override
    public void recycleFile(String pid) {
        if (StrUtil.isBlank(pid))
            return;
        // 回收文件即变更文件状态为300
        // 只回收200状态
        log.debug("recycle file({})...", pid);
        this.lambdaUpdate().eq(ResourceFile::getPid, pid)
            .eq(ResourceFile::getStatus, ResourceStatusEnum.NORMAL.getCode())
            .set(ResourceFile::getStatus, ResourceStatusEnum.RECYCLED.getCode())
            .set(ResourceFile::getUpdateTime, new Date())
            .update();

        // ResourceFile file = super.getById(pid);
        // if (Objects.nonNull(file)) {
        //     applicationEventPublisher.publishEvent(new DeleteResourceFileEvent(file.getCreatorId(), pid));
        // }
    }

    @Override
    public void recycleFiles(List<String> pidList) {
        if (CollectionUtil.isEmpty(pidList)) return;
        // 回收文件即变更文件状态为300
        // 只回收200状态
        log.debug("recycle files({})...", JSONUtil.toJsonStr(pidList));
        this.lambdaUpdate().in(ResourceFile::getPid, pidList)
            .eq(ResourceFile::getStatus, ResourceStatusEnum.NORMAL.getCode())
            .set(ResourceFile::getStatus, ResourceStatusEnum.RECYCLED.getCode())
            .set(ResourceFile::getUpdateTime, new Date())
            .update();

        // for (String pid : pidList) {
        //     ResourceFile file = super.getById(pid);
        //     if (Objects.isNull(file))
        //         continue;
        //     applicationEventPublisher.publishEvent(new DeleteResourceFileEvent(file.getCreatorId(), pid));
        // }
    }

    @Override
    public void recycleSubFiles(String supId) {
        log.debug("recycle sub files({supId: {}})...", supId);
        this.lambdaUpdate().eq(ResourceFile::getSupId, supId)
            .eq(ResourceFile::getStatus, ResourceStatusEnum.NORMAL.getCode())
            .set(ResourceFile::getStatus, ResourceStatusEnum.RECYCLED_LINKED.getCode())
            .set(ResourceFile::getUpdateTime, new Date())
            .update();

        // List<ResourceFile> subFiles = this.listSubFileDOs(supId, null, null, null);
        // for (ResourceFileBO subFile : subFiles) {
        //     applicationEventPublisher.publishEvent(new DeleteResourceFileEvent(subFile.getCreatorId(), subFile.getPid()));
        // }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void restoreFile(String pid, String tarSupId, String newName, ConflictModeEnum conflictHandle) {
        // 获取待处理文件
        ResourceFile file = this.getFileDO(this.select(), pid,
            LogicQuery.eq(ResourceStatusEnum.RECYCLED.groupCodes()), LogicQuery.none(), AssertEnum.IGNORE);
        this.getRestoreFileReady(file, tarSupId, newName);
        this.updateFileDoOnMoving(file, conflictHandle);
        log.debug("恢复文件({srcPid: {}, tarSupId: {}, newName: {}})完成.", pid, tarSupId, newName);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void restoreFiles(List<String> pidList, String tarSupId, ConflictModeEnum conflictHandle) {
        if (CollUtil.isEmpty(pidList)) return;
        List<ResourceFile> files = this.listFileDOs(this.select(), pidList,
            LogicQuery.eq(ResourceStatusEnum.RECYCLED.groupCodes()), LogicQuery.none());
        this.getRestoreFilesReady(files, tarSupId);
        this.updateFileDoOnMoving(files, tarSupId, conflictHandle);
        log.debug("批量恢复文件({tarSupId: {}})完成.", tarSupId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void restoreSubFiles(String srcSupId, String tarSupId, ConflictModeEnum conflictHandle) {
        List<ResourceFile> subFiles = this.listSubFileDOs(this.select(), srcSupId,
            LogicQuery.eq(ResourceStatusEnum.RECYCLED_LINKED.getCode()), LogicQuery.none());
        if (CollUtil.isEmpty(subFiles)) return;

        this.getRestoreFilesReady(subFiles, tarSupId);
        this.updateFileDoOnMoving(subFiles, tarSupId, conflictHandle);
        log.debug("批量移动子文件({srcSupId: {}, tarSupId: {}})完成.", srcSupId, tarSupId);
    }

    @Override
    public void deleteFile(String pid) {
        if (StrUtil.isBlank(pid)) return;
        // 删除文件即变更文件状态为400
        // 排除 被删除 状态的文件
        log.debug("delete file({})...", pid);
        List<Integer> deletedStatus = ResourceStatusEnum.DELETED.groupCodes();
        this.lambdaUpdate().eq(ResourceFile::getPid, pid)
            .notIn(ResourceFile::getStatus, deletedStatus)
            .set(ResourceFile::getStatus, ResourceStatusEnum.DELETED.getCode())
            .set(ResourceFile::getUpdateTime, new Date())
            .update();

        // ResourceFile file = super.getById(pid);
        // if (Objects.nonNull(file)) {
        //     applicationEventPublisher.publishEvent(new DeleteResourceFileEvent(file.getCreatorId(), pid));
        // }
    }

    @Override
    public void deleteFiles(List<String> pidList) {
        if (CollUtil.isEmpty(pidList)) return;
        // 删除文件即变更文件状态为400
        // 排除 被删除 状态的文件
        log.debug("delete files({})...", JSONUtil.toJsonStr(pidList));
        List<Integer> deletedStatus = ResourceStatusEnum.DELETED.groupCodes();
        this.lambdaUpdate().in(ResourceFile::getPid, pidList)
            .notIn(ResourceFile::getStatus, deletedStatus)
            .set(ResourceFile::getStatus, ResourceStatusEnum.DELETED.getCode())
            .set(ResourceFile::getUpdateTime, new Date())
            .update();

        // for (String pid : pidList) {
        //     ResourceFile file = super.getById(pid);
        //     if (Objects.isNull(file))
        //         continue;
        //     applicationEventPublisher.publishEvent(new DeleteResourceFileEvent(file.getCreatorId(), pid));
        // }
    }

    @Override
    public void deleteSubFiles(String supId) {
        if (StrUtil.isBlank(supId)) return;
        // 变更状态为 400
        // 排除 被删除 状态的文件
        log.debug("delete sub files({supId: {}})...", supId);
        List<Integer> deletedStatus = ResourceStatusEnum.DELETED.groupCodes();
        this.lambdaUpdate().eq(ResourceFile::getSupId, supId)
            .notIn(ResourceFile::getStatus, deletedStatus)
            // TODO: ZZL@09/24/2021 状态401
            .set(ResourceFile::getStatus, ResourceStatusEnum.DELETED.getCode())
            .set(ResourceFile::getUpdateTime, new Date())
            .update();

        // List<ResourceFile> subFiles = this.listSubFileDOs(supId, null, null, null);
        // for (ResourceFileBO subFile : subFiles) {
        //     applicationEventPublisher.publishEvent(new DeleteResourceFileEvent(subFile.getCreatorId(), subFile.getPid()));
        // }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void resurrectFile(String pid, String tarSupId, String newName, ConflictModeEnum conflictHandle) {
        // 获取待处理文件
        List<Integer> deletedStatus = ResourceStatusEnum.DELETED.groupCodes();
        ResourceFile file = this.getFileDO(this.select(), pid, LogicQuery.eq(deletedStatus), LogicQuery.none(), AssertEnum.IGNORE);
        this.getRestoreFileReady(file, tarSupId, newName);
        this.updateFileDoOnMoving(file, conflictHandle);
        log.debug("恢复文件({srcPid: {}, tarSupId: {}, newName: {}})完成.", pid, tarSupId, newName);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void resurrectFiles(List<String> pidList, String tarSupId, ConflictModeEnum conflictHandle) {
        List<Integer> deletedStatus = ResourceStatusEnum.DELETED.groupCodes();
        List<ResourceFile> files = this.listFileDOs(this.select(), pidList, LogicQuery.eq(deletedStatus), LogicQuery.none());
        this.updateFileDoOnMoving(files, tarSupId, conflictHandle);
        log.debug("批量恢复文件({tarSupId: {}})完成.", tarSupId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateFile(ResourceFileUpdateQuery file2Update) {
        Assert.isTrue(StrUtil.isNotBlank(file2Update.getPid()), "pid is blank");
        // empty string property => null
        ObjFieldUtil.isAllNullFields(file2Update, ObjFieldUtil.ConvertNull.EMPTY);
        // check property
        if (Objects.nonNull(file2Update.getIntegrity())) {
            FileIntegrityEnum.assertValid(file2Update.getIntegrity());
        }
        // update
        ResourceFile entity2Update = MapperUtil.nf().map(file2Update, ResourceFile.class);
        log.debug("update File info: {}", JSONUtil.toJsonStr(entity2Update));
        return super.updateById(entity2Update);
    }

    @Override
    public boolean updateFileByResourceKey(String resourceKey, ResourceFileUpdateQuery file2Update) {
        Assert.isTrue(StrUtil.isNotBlank(resourceKey), "pid is blank");
        // empty string property => null
        ObjFieldUtil.isAllNullFields(file2Update, ObjFieldUtil.ConvertNull.EMPTY);
        // check property
        if (Objects.nonNull(file2Update.getIntegrity())) {
            FileIntegrityEnum.assertValid(file2Update.getIntegrity());
        }
        // update
        ResourceFile entity2Update = MapperUtil.nf().map(file2Update, ResourceFile.class);
        entity2Update.pureBase();
        return super.lambdaUpdate().eq(ResourceFile::getResourceKey, resourceKey).update(entity2Update);
    }

    @Override
    public boolean updateFileIntegrity(String pid, Integer integrity) {
        Assert.isTrue(StrUtil.isNotBlank(pid), "fileId is blank");
        FileIntegrityEnum.assertValid(integrity);
        ResourceFile entity2Update = new ResourceFile();
        entity2Update.setPid(pid);
        entity2Update.setIntegrity(integrity);
        log.debug("update File integrity: {}", JSONUtil.toJsonStr(entity2Update));
        return super.updateById(entity2Update);
    }

    @Override
    @SneakyThrows
    public void downloadFileData(String pid, OutputStream ous, long startBytes) {
        // 获取待处理文件信息
        ResourceFile fileInfo = this.getFileDO(this.select(), pid, AssertEnum.FOUND);
        // 检查文件是否有本地缓存
        boolean exist = fileStorageService.exist(this.catalogService.getFilePath(fileInfo.getCatalog(), fileInfo.getResourceKey()));
        if (exist) {
            // 本地存储
            log.debug("服务器本地文件下载...");
            fileStorageService.read(this.catalogService.getFilePath(fileInfo.getCatalog(), fileInfo.getResourceKey()), ous, startBytes - 1);
        } else {
            // 云存储
            log.debug("S3云服务器文件下载...");
            boolean exists = s3FileService.exists(fileInfo.getResourceKey());
            Assert.isTrue(exists, "下载失败，远程文件({})丢失", fileInfo.getResourceKey());
            s3FileService.downloadFile(fileInfo.getResourceKey(), ous, startBytes - 1);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public File getFileData(String pid, AssertEnum assertEnum) {
        ResourceFile fileDO = super.getById(pid);
        AssertEnum.assertResult(assertEnum, fileDO, "文件信息");
        if (Objects.isNull(fileDO)) {
            return null;
        }
        String filePath = catalogService.getFilePath(fileDO.getCatalog(), fileDO.getResourceKey());
        File fileData = fileStorageService.getFile(filePath);
        if (!FileUtil.exist(fileData)) {
            // 本地文件不存在，从S3拉取
            FileStorageInfo storageInfo = new FileStorageInfo(fileDO.getCatalog(), fileDO.getResourceKey(), fileDO.getIsLocal(), fileDO.getIsCloud());
            try {
                storageModeService.dealFileStorage(storageInfo, StorageModeEnum.AVAILABLE_IN_LOCAL);
            } catch (Exception ignored) {}
        }
        AssertEnum.assertResult(assertEnum, FileUtil.exist(fileData), "文件数据");
        return fileData;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void dealStorageMode(String pid, Integer storageMode, boolean async) throws LostException {
        ResourceFile fileDO = this.getFileDO(this.select(), pid, AssertEnum.FOUND);
        FileStorageInfo storageInfo = new FileStorageInfo(fileDO.getCatalog(), fileDO.getResourceKey(), fileDO.getIsLocal(), fileDO.getIsCloud());
        StorageModeEnum storageModeEnum = StorageModeEnum.instance(storageMode);
        if (async) {
            ThreadUtil.execute(() -> {
                ThreadUtil.sleep(1000);

                storageModeService.dealFileStorage(storageInfo, storageModeEnum);
            });
        } else {
            storageModeService.dealFileStorage(storageInfo, storageModeEnum);
        }
    }

    // =============================================== private do method =============================================== //

    /**
     * 获取正常状态文件信息
     *
     * @param pid        文件ID
     * @param assertEnum 结果检查, null或IGNORE-跳过结果检查
     * @return 文件信息
     */
    private ResourceFile getFileDO(SFunction<ResourceFile, ?>[] cols, String pid, AssertEnum assertEnum) {
        return this.getFileDO(cols, pid,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()), assertEnum);
    }

    /**
     * 获取文件信息
     *
     * @param pid        文件ID, 必要
     * @param status     资源状态, 可选
     * @param integrity  文件完整性状态，可选
     * @param assertEnum 存在性校验
     * @return 文件信息
     */
    private ResourceFile getFileDO(SFunction<ResourceFile, ?>[] cols,
                                   String pid,
                                   LogicQuery<Integer> status,
                                   LogicQuery<Integer> integrity,
                                   AssertEnum assertEnum) {
        ResourceFile fileDO;
        if (StrUtil.isBlank(pid)) {
            fileDO = null;
        } else {
            LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryByPid(cols, pid, status, integrity);
            fileDO = query.one();
        }
        AssertEnum.assertResult(assertEnum, fileDO, String.format("文件(%s)", pid));
        return fileDO;
    }

    /**
     * 根pid列表获取文件信息列表
     *
     * @param cols      指定查询的列，空或null-查询所有
     * @param pidList   pid列表，必要
     * @param status    文件状态，可选
     * @param integrity 文件完整性，可选
     * @return 文件信息列表
     */
    private List<ResourceFile> listFileDOs(SFunction<ResourceFile, ?>[] cols,
                                           List<String> pidList,
                                           LogicQuery<Integer> status,
                                           LogicQuery<Integer> integrity) {
        if (CollUtil.isEmpty(pidList)) {
            return new ArrayList<>();
        }
        if (pidList.size() <= MybatisPlusUtil.MAX_IN_PARAMS) {
            LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryByPids(cols, pidList, status, integrity);
            return query.list();
        }
        List<ResourceFile> result = new ArrayList<>();
        List<List<String>> pidListGroup = ListUtil.partition(pidList, MybatisPlusUtil.MAX_IN_PARAMS);
        for (List<String> list : pidListGroup) {
            LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryByPids(cols, list, status, integrity);
            result.addAll(query.list());
        }
        return result;
    }

    /**
     * 获取指定目录下文件列表
     *
     * @param cols      指定查询的列，空或null-查询所有
     * @param supIdList supId列表，必要
     * @param status    文件状态，可选
     * @param integrity 文件完整性，可选
     * @return 指定目录下文件列表
     */
    private List<ResourceFile> listSubFileDOs(SFunction<ResourceFile, ?>[] cols,
                                              List<String> supIdList,
                                              LogicQuery<Integer> status,
                                              LogicQuery<Integer> integrity) {
        if (CollUtil.isEmpty(supIdList)) {
            return new ArrayList<>();
        }
        if (supIdList.size() <= MybatisPlusUtil.MAX_IN_PARAMS) {
            LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryBySupIds(cols, supIdList, status, integrity);
            return query.list();
        }
        List<ResourceFile> result = new ArrayList<>();
        List<List<String>> pidListGroup = ListUtil.partition(supIdList, MybatisPlusUtil.MAX_IN_PARAMS);
        for (List<String> list : pidListGroup) {
            LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryBySupIds(cols, list, status, integrity);
            result.addAll(query.list());
        }
        return result;
    }

    /**
     * 获取指定目录下文件列表
     *
     * @param cols      指定查询的列，空或null-查询所有
     * @param supId     目录ID
     * @param status    文件状态，可选
     * @param integrity 文件完整性，可选
     * @return 指定目录下文件列表
     */
    private List<ResourceFile> listSubFileDOs(SFunction<ResourceFile, ?>[] cols,
                                              String supId,
                                              LogicQuery<Integer> status,
                                              LogicQuery<Integer> integrity) {
        if (StrUtil.isBlank(supId)) {
            return new ArrayList<>();
        }
        LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryBySupId(cols, supId, null, status, integrity);
        return query.list();
    }

    /**
     * 获取用户指定存储空间下的文件列表
     *
     * @param cols      指定查询的列，空或null-查询所有
     * @param userId    用户ID
     * @param spaceId   存储空间ID
     * @param status    文件状态，可选
     * @param integrity 文件完整性，可选
     * @return 用户指定存储空间下的文件列表
     */
    private List<ResourceFile> listUserFileDOs(SFunction<ResourceFile, ?>[] cols,
                                               String userId, String spaceId,
                                               LogicQuery<Integer> status,
                                               LogicQuery<Integer> integrity) {
        LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryBySpace(cols, userId, spaceId, status, integrity);
        return query.list();
    }

    /**
     * 保存文件信息
     *
     * @param fileDO         文件信息
     * @param conflictHandle 文件名冲突解决模式
     * @return 保存后的文件信息，含生成的PID（若提交时未指定PID）
     */
    private ResourceFile saveFileDO(ResourceFile fileDO, ConflictModeEnum conflictHandle) {
        log.debug("save file info({})...", fileDO);
        if (Objects.isNull(fileDO)) {
            return null;
        }
        this.checkSaveParams(fileDO);
        this.checkSupFolder(fileDO);
        return this.dealNameConflict(fileDO, conflictHandle, renamedFile -> {
            super.save(renamedFile);
            log.debug("save file info({}) completed.", renamedFile);
            return renamedFile;
        });
    }

    private void saveFileDOs(List<ResourceFile> fileDOList, ConflictModeEnum conflictHandle) {
        log.debug("bath save file info...");
        if (CollUtil.isEmpty(fileDOList)) {
            return;
        }
        this.checkSaveParams(fileDOList);
        this.checkSupFolder(fileDOList);
        for (ResourceFile fileDO : fileDOList) {
            this.dealNameConflict(fileDO, conflictHandle, renamedFile -> {
                super.save(renamedFile);
                log.debug("save file info({}) completed.", Json.toJsonString(renamedFile));
                return renamedFile;
            });
        }
    }

    private void saveFileDOs(List<ResourceFile> fileDOList, String tarSupId, ConflictModeEnum conflictHandle) {
        log.debug("bath save file info...");
        if (CollUtil.isEmpty(fileDOList)) {
            return;
        }
        this.checkSaveParams(fileDOList);
        this.checkSupFolder(fileDOList, tarSupId);
        for (ResourceFile fileDO : fileDOList) {
            this.dealNameConflict(fileDO, conflictHandle, renamedFile -> {
                super.save(renamedFile);
                log.debug("save file info({}) completed.", Json.toJsonString(renamedFile));
                return renamedFile;
            });
        }
    }

    private ResourceFile updateFileDoOnMoving(ResourceFile file2Update, ConflictModeEnum conflictHandle) {
        if (Objects.isNull(file2Update)) {
            return null;
        }
        if (StrUtil.isNotBlank(file2Update.getSupId())) {
            this.checkSupFolder(file2Update);
        }
        // 更新
        return this.dealNameConflict(file2Update, conflictHandle, renamedFile -> {
            this.updateFileInfoOnMoving(file2Update);
            return renamedFile;
        });
    }

    private void updateFileDoOnMoving(List<ResourceFile> files2Update, String tarSupId, ConflictModeEnum conflictHandle) {
        if (CollUtil.isEmpty(files2Update)) {
            return;
        }
        if (StrUtil.isNotBlank(tarSupId)) {
            this.checkSupFolder(files2Update, tarSupId);
        } else {
            this.checkSupFolder(files2Update);
        }
        for (ResourceFile file2Update : files2Update) {
            this.dealNameConflict(file2Update, conflictHandle, renamedFile -> {
                this.updateFileInfoOnMoving(file2Update);
                return renamedFile;
            });
        }
    }

    private void updateFileInfoOnMoving(ResourceFile file2Update) {
        ResourceFile entity = new ResourceFile();
        entity.setPid(file2Update.getPid());
        entity.setSpaceId(file2Update.getSpaceId());
        entity.setSupId(file2Update.getSupId());
        entity.setName(file2Update.getName());
        entity.setStatus(file2Update.getStatus());
        entity.setCreatorId(file2Update.getCreatorId());
        entity.setEditorId(file2Update.getEditorId());
        super.updateById(entity);
    }

    /**
     * 覆盖删除文件
     */
    private void coverFileDO(String pid) {
        if (StrUtil.isBlank(pid))
            return;
        // 删除文件即变更文件状态为400
        // 排除 被删除 状态的文件
        List<Integer> deletedStatus = ResourceStatusEnum.DELETED.groupCodes();
        this.lambdaUpdate().eq(ResourceFile::getPid, pid)
            .notIn(ResourceFile::getStatus, deletedStatus)
            .set(ResourceFile::getStatus, ResourceStatusEnum.DELETED_COVER.getCode())
            .set(ResourceFile::getCreateTime, new Date())
            .update();
    }

    // ================================================ private method ================================================ //

    @SafeVarargs
    private final SFunction<ResourceFile, ?>[] select(SFunction<ResourceFile, ?>... cols) {
        return cols;
    }

    private LambdaQueryChainWrapper<ResourceFile> buildQueryByPid(SFunction<ResourceFile, ?>[] cols,
                                                                  String pid,
                                                                  LogicQuery<Integer> status,
                                                                  LogicQuery<Integer> integrity) {
        LambdaQueryChainWrapper<ResourceFile> query = super.lambdaQuery()
            .eq(ResourceFile::getPid, pid);
        if (ArrayUtil.isNotEmpty(cols)) {
            query.select(cols);
        }
        StorageUtil.addCondition(query, status, integrity);
        MybatisPlusUtil.limitOne(query);
        return query;
    }

    private LambdaQueryChainWrapper<ResourceFile> buildQueryByPids(SFunction<ResourceFile, ?>[] cols,
                                                                   List<String> pidList,
                                                                   LogicQuery<Integer> status,
                                                                   LogicQuery<Integer> integrity) {
        LambdaQueryChainWrapper<ResourceFile> query = super.lambdaQuery()
            .in(CollUtil.isNotEmpty(pidList), ResourceFile::getPid, pidList);
        if (ArrayUtil.isNotEmpty(cols)) {
            query.select(cols);
        }
        StorageUtil.addCondition(query, status, integrity);
        return query;
    }

    private LambdaQueryChainWrapper<ResourceFile> buildQueryBySupId(SFunction<ResourceFile, ?>[] cols,
                                                                    String supId, String fileName,
                                                                    LogicQuery<Integer> status,
                                                                    LogicQuery<Integer> integrity) {
        LambdaQueryChainWrapper<ResourceFile> query = this.lambdaQuery()
            .eq(StrUtil.isNotBlank(supId), ResourceFile::getSupId, supId)
            .eq(StrUtil.isNotBlank(fileName), ResourceFile::getName, fileName);
        if (ArrayUtil.isNotEmpty(cols)) {
            query.select(cols);
        }
        StorageUtil.addCondition(query, status, integrity);
        return query;
    }

    private LambdaQueryChainWrapper<ResourceFile> buildQueryBySupIds(SFunction<ResourceFile, ?>[] cols,
                                                                     List<String> supIdList,
                                                                     LogicQuery<Integer> status,
                                                                     LogicQuery<Integer> integrity) {
        LambdaQueryChainWrapper<ResourceFile> query = super.lambdaQuery()
            .in(CollUtil.isNotEmpty(supIdList), ResourceFile::getSupId, supIdList);
        if (ArrayUtil.isNotEmpty(cols)) {
            query.select(cols);
        }
        StorageUtil.addCondition(query, status, integrity);
        return query;
    }

    private LambdaQueryChainWrapper<ResourceFile> buildQueryBySpace(SFunction<ResourceFile, ?>[] cols,
                                                                    String userId, String spaceId,
                                                                    LogicQuery<Integer> status,
                                                                    LogicQuery<Integer> integrity) {
        LambdaQueryChainWrapper<ResourceFile> query = this.lambdaQuery()
            .eq(StrUtil.isNotBlank(userId), ResourceFile::getCreatorId, userId)
            .eq(StrUtil.isNotBlank(spaceId), ResourceFile::getSpaceId, spaceId);
        if (ArrayUtil.isNotEmpty(cols)) {
            query.select(cols);
        }
        StorageUtil.addCondition(query, status, integrity);
        return query;
    }

    private LambdaQueryChainWrapper<ResourceFile> buildQueryBySpaces(SFunction<ResourceFile, ?>[] cols,
                                                                     String userId, List<String> spaceIds,
                                                                     LogicQuery<Integer> status,
                                                                     LogicQuery<Integer> integrity) {
        LambdaQueryChainWrapper<ResourceFile> query = this.lambdaQuery()
            .eq(StrUtil.isNotBlank(userId), ResourceFile::getCreatorId, userId)
            .in(CollUtil.isNotEmpty(spaceIds), ResourceFile::getSpaceId, spaceIds);
        if (ArrayUtil.isNotEmpty(cols)) {
            query.select(cols);
        }
        StorageUtil.addCondition(query, status, integrity);
        return query;
    }


    private LambdaQueryWrapper<ResourceFile> buildSumLengthQuery(LogicQuery<Integer> status, LogicQuery<Integer> integrity) {
        LambdaQueryWrapper<ResourceFile> query = Wrappers.<ResourceFile>query()
            .select("IFNULL(SUM(IFNULL(length,0)),0) AS `total`").lambda();
        StorageUtil.addStatusCondition(query, status);
        StorageUtil.addIntegrityCondition(query, integrity);
        return query;
    }

    private long getSumLength(LambdaQueryWrapper<ResourceFile> query) {
        Map<String, Object> map = super.getMap(query);
        Object total = map.get("total");
        return Objects.isNull(total) ? 0L : NumberUtil.parseLong(String.valueOf(total));
    }

    /**
     * 检查父目录有效性，设置(父目录的)关联信息
     */
    private void checkSupFolder(ResourceFile fileInfo) {
        ResourceFolderStructBO supFolder = folderService.getFolder(fileInfo.getSupId(), AssertEnum.FOUND);
        fileInfo.setSpaceId(supFolder.getSpaceId());
        fileInfo.setCreatorId(supFolder.getCreatorId());
        fileInfo.setEditorId(supFolder.getCreatorId());
    }

    private void checkSupFolder(List<ResourceFile> fileInfoList) {
        Map<String, List<ResourceFile>> collect = fileInfoList.stream()
            .collect(Collectors.groupingBy(ResourceFile::getSupId));
        collect.forEach((supId, resourceFiles) -> this.checkSupFolder(resourceFiles, supId));
    }

    private void checkSupFolder(List<ResourceFile> fileInfoList, String supId) {
        ResourceFolderStructBO supFolder = folderService.getFolder(supId, AssertEnum.FOUND);
        fileInfoList.forEach(fileInfo -> {
            fileInfo.setSupId(supId);
            fileInfo.setSpaceId(supFolder.getSpaceId());
            fileInfo.setCreatorId(supFolder.getCreatorId());
            fileInfo.setEditorId(supFolder.getEditorId());
        });
    }

    /**
     * 校验同名约束
     *
     * @param pid   文件ID
     * @param supId 文件目录ID
     * @param name  文件名
     * @return 存在的同名文件
     */
    private ResourceFile checkSameName(String pid, String supId, String name) {
        LambdaQueryChainWrapper<ResourceFile> query = this.buildQueryBySupId(this.select(), supId, name,
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()), LogicQuery.none());
        List<ResourceFile> sameNameFiles = query.list();
        return sameNameFiles.stream()
            .filter(sameNameFile -> !Objects.equals(pid, sameNameFile.getPid()))
            .findAny().orElse(null);
    }

    /**
     * 自动重命名同名文件
     *
     * @param supId    目录ID
     * @param fileName 重复文件名
     * @return 新名称
     */
    private String renameNext(String supId, String fileName) {
        int startIndex = 1;
        String suffix = FileUtil.extName(fileName);
        suffix = StrUtil.isBlank(suffix) ? suffix : ("." + suffix);
        String mainName = FileUtil.mainName(fileName);
        String namePrefixHead = mainName + PathConsts.COPY_SUFFIX_HEAD;
        LambdaQueryChainWrapper<ResourceFile> query = this.lambdaQuery()
            .eq(ResourceFile::getSupId, supId)
            .eq(ResourceFile::getStatus, ResourceStatusEnum.NORMAL.getCode())
            .likeRight(ResourceFile::getName, namePrefixHead)
            .orderByDesc(ResourceFile::getName);
        MybatisPlusUtil.limitOne(query);
        ResourceFile indexMaxFile = query.one();
        if (Objects.isNull(indexMaxFile)) {
            return StrUtil.concat(true,
                namePrefixHead, String.valueOf(startIndex), PathConsts.COPY_SUFFIX_END, suffix);
        }
        String name = indexMaxFile.getName();
        String indexStr = name.substring(namePrefixHead.length(), name.lastIndexOf(PathConsts.COPY_SUFFIX_END));
        try {
            startIndex = Integer.parseInt(indexStr) + 1;
        } catch (NumberFormatException ignored) {}
        return StrUtil.concat(true,
            namePrefixHead, String.valueOf(startIndex), PathConsts.COPY_SUFFIX_END, suffix);
    }

    /**
     * 文件信息保存参数校验
     */
    private void checkSaveParams(ResourceFile file2Save) {
        // verify blank properties
        Assert.isTrue(StrUtil.isNotBlank(file2Save.getSupId()), "父目录为空");
        Assert.isTrue(StrUtil.isNotBlank(file2Save.getName()), "文件名为空");
        Assert.isTrue(StrUtil.isNotBlank(file2Save.getCatalog()), "catalog为空");
        // 检查文件名是否有windows违规字符
        boolean isValidName = !FileNameUtil.containsInvalid(file2Save.getName());
        Assert.isTrue(isValidName, "文件名含有违规字符");
        // default value
        file2Save.setSuffix(FileUtil.getSuffix(file2Save.getName()));
        if (StrUtil.isBlank(file2Save.getPid())) {
            file2Save.setPid(SnowFlakeUtil.nextIdStr());
        }
        if (Objects.isNull(file2Save.getStatus())) {
            file2Save.setStatus(ResourceStatusEnum.NORMAL.getCode());
        }
        if (Objects.isNull(file2Save.getIntegrity())) {
            file2Save.setIntegrity(FileIntegrityEnum.NORMAL.getCode());
        }
        if (Objects.isNull(file2Save.getVid())) {
            file2Save.setVid(SnowFlakeUtil.nextIdStr());
        }
        if (StrUtil.isBlank(file2Save.getResourceKey())) {
            file2Save.setResourceKey(file2Save.getPid());
        }
        // blank value
        // ObjFieldUtil.convertNull2Default(file2Save);
        // 去除时间
        file2Save.pureTime();
    }

    private void checkSaveParams(List<ResourceFile> files2Save) {
        files2Save.forEach(this::checkSaveParams);
    }

    private void getCopyFileReady(ResourceFile srcFile, String tarSupId, String newName) {
        srcFile.setPid(null);
        srcFile.setVid(null);
        if (StrUtil.isNotBlank(tarSupId)) {
            srcFile.setSupId(tarSupId);
        }
        if (StrUtil.isNotBlank(newName)) {
            // 检查文件名是否有windows违规字符
            boolean isValidName = !FileNameUtil.containsInvalid(newName);
            Assert.isTrue(isValidName, "文件名含有违规字符");
            srcFile.setName(newName);
        }
        srcFile.setStatus(ResourceStatusEnum.NORMAL.getCode());
    }

    private void getCopyFilesReady(List<ResourceFile> srcFiles, String tarSupId) {
        srcFiles.forEach(srcFile -> this.getCopyFileReady(srcFile, tarSupId, null));
    }

    /**
     * @return 是否需要移动
     */
    private boolean getMoveFileReady(ResourceFile srcFile, String tarSupId, String newName) {
        if (StrUtil.isBlank(newName)) {
            newName = srcFile.getName();
        } else {
            // 检查文件名是否有windows违规字符
            boolean isValidName = !FileNameUtil.containsInvalid(newName);
            Assert.isTrue(isValidName, "文件名含有违规字符");
        }
        if (StrUtil.isBlank(tarSupId)) {
            tarSupId = srcFile.getSupId();
        }
        // 检查是否无需更新，父目录与名字无变动
        if (Objects.equals(srcFile.getSupId(), tarSupId)
            && Objects.equals(srcFile.getName(), newName)) {
            return false;
        }
        srcFile.setSupId(tarSupId);
        srcFile.setName(newName);

        return true;
    }

    private void getMoveFilesReady(List<ResourceFile> srcFiles, String tarSupId) {
        srcFiles.removeIf(srcFile -> !this.getMoveFileReady(srcFile, tarSupId, null));
    }

    private void getRestoreFileReady(ResourceFile srcFile, String tarSupId, String newName) {
        if (StrUtil.isNotBlank(tarSupId)) {
            srcFile.setSupId(tarSupId);
        }
        if (StrUtil.isNotBlank(newName)) {
            // 检查文件名是否有windows违规字符
            boolean isValidName = !FileNameUtil.containsInvalid(newName);
            Assert.isTrue(isValidName, "文件名含有违规字符");
            srcFile.setName(newName);
        }
        srcFile.setStatus(ResourceStatusEnum.NORMAL.getCode());
    }

    private void getRestoreFilesReady(List<ResourceFile> srcFiles, String tarSupId) {
        srcFiles.forEach(srcFile -> this.getRestoreFileReady(srcFile, tarSupId, null));
    }

    /**
     * 处理同名冲突
     *
     * @param srcFile        源文件
     * @param conflictHandle 冲突解决方案
     * @param postHandle     后续处理
     * @return 最终保存的文件
     */
    private ResourceFile dealNameConflict(ResourceFile srcFile, ConflictModeEnum conflictHandle, DealNameConflictHandler postHandle) {
        ResourceFile sameNameFile = this.checkSameName(srcFile.getPid(), srcFile.getSupId(), srcFile.getName());
        if (Objects.nonNull(sameNameFile)) {
            switch (conflictHandle) {
                case ALLOW:
                    log.debug("允许存在同名文件({})", sameNameFile.getName());
                    break;
                case RENAME_SRC:
                    String rename = this.renameNext(srcFile.getSupId(), srcFile.getName());
                    log.debug("同名文件({})存在，重命名源文件({oldName: {}, newName: {}}).", sameNameFile.getPid(), srcFile.getName(), rename);
                    srcFile.setName(rename);
                    break;
                case DELETE_SRC:
                    log.debug("同名文件({})存在，删除源文件({}).", sameNameFile.getPid(), srcFile.getPid());
                    this.deleteFile(srcFile.getPid());
                    return sameNameFile;
                case DELETE_SRC_PURE:
                    log.debug("同名文件({})存在，删除源文件({}).", sameNameFile.getPid(), srcFile.getPid());
                    this.deleteFilePure(srcFile);
                    return sameNameFile;
                case COVER_TAR:
                    log.debug("同名文件({})存在，使用源文件({})覆盖.", sameNameFile.getPid(), srcFile.getPid());
                    srcFile.setVid(sameNameFile.getVid());
                    // TODO: ZZL@09/25/2021 状态402
                    // this.coverFile(sameNameFile.getPid());
                    this.deleteFile(sameNameFile.getPid());
                    break;
                case COVER_TAR_PURE:
                    log.debug("同名文件({})存在，使用源文件({})覆盖(pure模式).", sameNameFile.getPid(), srcFile.getPid());
                    this.deleteFilePure(sameNameFile);
                    break;
                case THROW_ERR:
                    ResourceFileBO srcFileBO = MapperUtil.nf().map(srcFile, ResourceFileBO.class);
                    ResourceFileBO sameNameFileBO = MapperUtil.nf().map(sameNameFile, ResourceFileBO.class);
                    throw FileConflictException.build("存在同名文件", new ResourceFileConflictBO(srcFileBO, sameNameFileBO));
            }
        }
        return postHandle.deal(srcFile);
    }

    private boolean isResourceInUse(String resourceKey, String... excludeFileIds) {
        List<Integer> deleted = ResourceStatusEnum.DELETED.groupCodes();
        Integer count = super.lambdaQuery().eq(ResourceFile::getResourceKey, resourceKey)
            .notIn(ResourceFile::getStatus, deleted)
            .notIn(ArrayUtil.isNotEmpty(excludeFileIds), ResourceFile::getPid, Arrays.asList(excludeFileIds))
            .count();
        return !Objects.equals(count, 0);
    }

    private void deleteFilePure(ResourceFile file) {
        super.removeById(file.getPid());
        if (!isResourceInUse(file.getResourceKey(), file.getPid())) {
            fileStorageService.delete(catalogService.getFilePath(file.getCatalog(), file.getResourceKey()), true);
            s3FileService.deleteFile(file.getResourceKey());
            inxChunkService.deleteChunks(file.getPid(), storageConfig.getPersonalSpaceCacheCatalog());
        }
    }

    /**
     * 名称冲突后续处理接口
     */
    private interface DealNameConflictHandler {
        ResourceFile deal(ResourceFile renamedFile);
    }

}
